using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using DirectShowLib;

namespace com.bodurov.CameraToJpegFilesUI
{
    internal class Camera : ISampleGrabberCB, IDisposable
    {
        private bool _isWaiting;
        private IntPtr _pointerToBitmap = IntPtr.Zero;
        private ManualResetEvent _resetEvent;
        private IFilterGraph2 _filterGraph2;
        private IPin _stillPin;


        public event EventHandler<ImageCapturedEventArgs> ImageCaptured;

        public Camera(int width, int height, short pitsPerPixel)
        {
            Conf = new CameraConfig();
            // get collection of video devices
            var devices = DsDevice.GetDevicesOfCat(FilterCategory.VideoInputDevice);
            if (devices.Length == 0)
            {
                throw new ApplicationException("No video capture devices found!");
            }

            try
            {
                // Set up the DirectShow graph
                SetupGraph(devices[0], width, height, pitsPerPixel);

                // cross thread communication
                this._resetEvent = new ManualResetEvent(false);
            }
            catch
            {
                Dispose();
                throw;
            }
        }
        // Destructor
        ~Camera()
        {
            Dispose();
        }

        public CameraConfig Conf { get; private set; }
        public int VideoWidth { get; private set; }
        public int VideoHeight { get; private set; }
        public int Stride { get; private set; }

        public void RequestPointerToBitmap()
        {
            if (this._resetEvent == null) return;
            // get ready to wait for new image
            this._resetEvent.Reset();
            this._pointerToBitmap = Marshal.AllocCoTaskMem(Math.Abs(this.Stride) * this.VideoHeight);

            try
            {
                this._isWaiting = true;

                if (!this._resetEvent.WaitOne(9000, false))
                {
                    throw new Exception("Timeout waiting to get picture");
                }

                this.OnImageCaptured();
            }
            catch
            {
                if (this._pointerToBitmap != IntPtr.Zero) Marshal.FreeCoTaskMem(this._pointerToBitmap);
                this._pointerToBitmap = IntPtr.Zero;
                throw;
            }
        }

        protected virtual void OnImageCaptured()
        {
            if (this.ImageCaptured != null)
            {
                this.ImageCaptured(this, new ImageCapturedEventArgs(this._pointerToBitmap));
            }
        }

        private void SetupGraph(DsDevice dev, int width, int height, short bitsPerPixel)
        {
            ISampleGrabber sampGrabber = null;
            IPin pSampleIn = null;

            this._filterGraph2 = new FilterGraph() as IFilterGraph2;

            try
            {
                if (this._filterGraph2 == null) throw new ApplicationException("Cannot instantiate filter graph");
                // add the video input device
                IBaseFilter capFilter = null;
                int hr = this._filterGraph2.AddSourceFilterForMoniker(dev.Mon, null, dev.Name, out capFilter);
                DsError.ThrowExceptionForHR(hr);

                IPin capturePin = null;
                IPin inputPin = null;


                var splitter = new SmartTee() as IBaseFilter;

                try
                {
                    hr = this._filterGraph2.AddFilter(splitter, "SmartTee");
                    DsError.ThrowExceptionForHR(hr);

                    // Find the capture pin from the video device and the input pin for the splitter, and connnect them
                    capturePin = DsFindPin.ByCategory(capFilter, PinCategory.Capture, 0);
                    inputPin = DsFindPin.ByDirection(splitter, PinDirection.Input, 0);

                    hr = this._filterGraph2.Connect(capturePin, inputPin);
                    DsError.ThrowExceptionForHR(hr);

                    // Now set the still pins (from the splitter)
                    this._stillPin = DsFindPin.ByName(splitter, "Preview");

                    // set width, height, 
                    if (height + width + bitsPerPixel > 0)
                    {
                        SetConfigParms(capturePin, width, height, bitsPerPixel);
                    }
                }
                finally
                {
                    if (capturePin != null)
                    {
                        Marshal.ReleaseComObject(capturePin);
                    }
                    if (capturePin != inputPin && inputPin != null)
                    {
                        Marshal.ReleaseComObject(inputPin);
                    }
                    if (capturePin != splitter && splitter != null)
                    {
                        Marshal.ReleaseComObject(splitter);
                    }
                }


                sampGrabber = new SampleGrabber() as ISampleGrabber;

                // configure the sample grabber
                var baseGrabFlt = sampGrabber as IBaseFilter;
                ConfigureSampleGrabber(sampGrabber);
                pSampleIn = DsFindPin.ByDirection(baseGrabFlt, PinDirection.Input, 0);

                // default video renderer
                var pRenderer = new VideoRendererDefault() as IBaseFilter;
                hr = this._filterGraph2.AddFilter(pRenderer, "Renderer");
                DsError.ThrowExceptionForHR(hr);

                // add the sample grabber to the graph
                hr = this._filterGraph2.AddFilter(baseGrabFlt, "Ds.NET Grabber");
                DsError.ThrowExceptionForHR(hr);


                // Connect the still pin to the sample grabber
                hr = this._filterGraph2.Connect(this._stillPin, pSampleIn);
                DsError.ThrowExceptionForHR(hr);

                // get the video properties
                SaveSizeInfo(sampGrabber);

                // Start the graph
                var mediaCtrl = this._filterGraph2 as IMediaControl;


                var control = capFilter as IAMCameraControl;

                // uncomment this to see the default value
                //int val;
                //CameraControlFlags flags;
                //control.Get(CameraControlProperty.Focus, out val, out flags);


                // disable Auto focus (which is by default and set the focus to 0)
                if (control != null)
                {
                    control.Set(CameraControlProperty.Focus, Conf.Focus, CameraControlFlags.Manual);
                    control.Set(CameraControlProperty.Zoom, Conf.Zoom, CameraControlFlags.Manual);
                    control.Set(CameraControlProperty.Pan, Conf.Pan, CameraControlFlags.Manual);
                    control.Set(CameraControlProperty.Tilt, Conf.Tilt, CameraControlFlags.Manual);
                    control.Set(CameraControlProperty.Exposure, Conf.Exposure, CameraControlFlags.Manual);
                }

                var procAmp = capFilter as IAMVideoProcAmp;
                if (procAmp != null)
                {
                    procAmp.Set(VideoProcAmpProperty.Brightness, Conf.Brightness, VideoProcAmpFlags.Manual);
                    procAmp.Set(VideoProcAmpProperty.Contrast, Conf.Contrast, VideoProcAmpFlags.Manual);
                    procAmp.Set(VideoProcAmpProperty.Saturation, Conf.Saturation, VideoProcAmpFlags.Manual);
                    procAmp.Set(VideoProcAmpProperty.Sharpness, Conf.Sharpness, VideoProcAmpFlags.Manual);
                    procAmp.Set(VideoProcAmpProperty.WhiteBalance, Conf.WhiteBalance, VideoProcAmpFlags.Manual);
                    procAmp.Set(VideoProcAmpProperty.BacklightCompensation, Conf.BacklightCompensation, VideoProcAmpFlags.Manual);
                }


                hr = mediaCtrl.Run();
                DsError.ThrowExceptionForHR(hr);
            }
            finally
            {
                if (sampGrabber != null)
                {
                    Marshal.ReleaseComObject(sampGrabber);
                }
                if (pSampleIn != null)
                {
                    Marshal.ReleaseComObject(pSampleIn);
                }
            }
        }

        private void SaveSizeInfo(ISampleGrabber sampGrabber)
        {
            // find the media type from the SampleGrabber
            var media = new AMMediaType();

            var hr = sampGrabber.GetConnectedMediaType(media);
            DsError.ThrowExceptionForHR(hr);

            if ((media.formatType != FormatType.VideoInfo) || (media.formatPtr == IntPtr.Zero))
            {
                throw new NotSupportedException("Unknown Grabber Media Format");
            }

            var videoInfoHeader = (VideoInfoHeader)Marshal.PtrToStructure(media.formatPtr, typeof(VideoInfoHeader));
            this.VideoWidth = videoInfoHeader.BmiHeader.Width;
            this.VideoHeight = videoInfoHeader.BmiHeader.Height;
            this.Stride = this.VideoWidth * (videoInfoHeader.BmiHeader.BitCount / 8);

            DsUtils.FreeAMMediaType(media);
        }

        private void ConfigureSampleGrabber(ISampleGrabber sampGrabber)
        {
            var media = new AMMediaType
            {
                majorType = MediaType.Video,
                subType = MediaSubType.RGB24,
                formatType = FormatType.VideoInfo
            };

            // set the media type to Video/RBG24
            int hr = sampGrabber.SetMediaType(media);
            DsError.ThrowExceptionForHR(hr);

            DsUtils.FreeAMMediaType(media);

            // configure the samplegrabber - number 1 here means that ISampleGrabberCB.BufferCB() will be used and not ISampleGrabberCB.SampleCB()
            hr = sampGrabber.SetCallback(this, 1);
            DsError.ThrowExceptionForHR(hr);
        }


        private static void SetConfigParms(IPin pin, int width, int height, short bitsPerPixel)
        {
            AMMediaType media;
            VideoInfoHeader v;

            var videoStreamConfig = pin as IAMStreamConfig;

            if (videoStreamConfig == null) throw new ApplicationException("Cannot cast IPin to IAMStreamConfig");

            int hr = videoStreamConfig.GetFormat(out media);
            DsError.ThrowExceptionForHR(hr);

            try
            {
                // copy out the videoinfoheader
                v = new VideoInfoHeader();

                Marshal.PtrToStructure(media.formatPtr, v);

                if (width > 0)
                {
                    v.BmiHeader.Width = width;
                }

                if (height > 0)
                {
                    v.BmiHeader.Height = height;
                }

                if (bitsPerPixel > 0)
                {
                    v.BmiHeader.BitCount = bitsPerPixel;
                }

                // copy the media structure back
                Marshal.StructureToPtr(v, media.formatPtr, false);

                // set the new format
                hr = videoStreamConfig.SetFormat(media);
                DsError.ThrowExceptionForHR(hr);
            }
            finally
            {
                DsUtils.FreeAMMediaType(media);
            }
        }

        private void CloseInterfaces()
        {
            try
            {
                if (this._filterGraph2 != null)
                {
                    var mediaCtrl = this._filterGraph2 as IMediaControl;

                    if (mediaCtrl != null) mediaCtrl.Stop();
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
            }

            if (this._filterGraph2 != null)
            {
                this._filterGraph2.Disconnect(this._stillPin);
                Marshal.ReleaseComObject(this._filterGraph2);
                this._filterGraph2 = null;
            }
            if (this._stillPin != null)
            {
                this._stillPin.Disconnect();
                Marshal.ReleaseComObject(this._stillPin);
                this._stillPin = null;
            }
        }



        [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory")]
        private static extern void CopyMemory(IntPtr Destination, IntPtr Source, [MarshalAs(UnmanagedType.U4)] int Length);

        #region Implementation of ISampleGrabberCB

        /// <summary> NOT USED. </summary>
        int ISampleGrabberCB.SampleCB(double SampleTime, IMediaSample pSample)
        {
            Marshal.ReleaseComObject(pSample);
            return 0;
        }

        int ISampleGrabberCB.BufferCB(double sampleTime, IntPtr source, int bufferLen)
        {
            if (this._isWaiting)
            {
                this._isWaiting = false;

                if (this._pointerToBitmap != IntPtr.Zero)
                {
                    CopyMemory(this._pointerToBitmap, source, bufferLen);

                    this._resetEvent.Set();
                }
            }

            return 0;
        }

        #endregion

        #region Implementation of IDisposable

        public void Dispose()
        {
            CloseInterfaces();
            CloseResetEvent();
            FreePointerMemory();
        }

        #endregion

        private void CloseResetEvent()
        {
            if (this._resetEvent != null)
            {
                this._resetEvent.Close();
                this._resetEvent = null;
            }
        }
        private void FreePointerMemory()
        {
            if (this._pointerToBitmap != IntPtr.Zero)
            {
                Marshal.FreeCoTaskMem(this._pointerToBitmap);
                this._pointerToBitmap = IntPtr.Zero;
            }
        }
        public void FreePointerMemoryAndRequestNew()
        {
            this.FreePointerMemory();
            this.RequestPointerToBitmap();
        }
    }
}